<?php

/**
 * REDIS 分布式加解锁类
 */
declare(strict_types=1);
namespace Service\AppService;
use Service\BaseService;

class RedisRedLock extends BaseService
{
    protected $redis;
    private $lockedNames = [];
    const REDIS_LOCK_KEY_PREFIX = 'redis:lock:';
    const REDIS_LOCK_KEY = 'REDIS_GOODS_STOCK';
    const REDIS_DB = 11;
    public function __construct() 
    {
        $this->redis = $this->getRedisObj();
        $this->redis->select(self::REDIS_DB);
    }
    
    /**
     * 同时加锁多个key
     */
    public function getMultiLock( array $locksArr = [],string $kVal = null, int $expire = 35, int $sleep = 1 )
    {
        $result = [];
        $count = count($locksArr);
        $wg = new WaitGroup();
        $wg->add($count);
        
        $isMultiLockFailedCount = [];
        //利用协程的方式同时加上多把锁
        foreach ($locksArr as $key => $val ) 
        {
            co(function () use ($wg,$key,$val,& $result,$kVal) 
            {
                $event_id = $val['event_id'];
                $isLockOk = $this->lock($event_id, $kVal);
                if( ! $isLockOk ) {
                    $isMultiLockFailedCount[$event_id] = 1;
                }
                $wg->done();
            });
        }
        
        $wg->wait();
        
        return $isMultiLockFailedCount ? false : true;
    }
    
    /**
     * 同时解锁
     * @param string $name
     * @param string $val
     */
    public function getMultiUnLock(array $locksArr, $kVal )
    {
        $result = [];
        $count = count($locksArr);
        $wg = new WaitGroup();
        $wg->add($count);
        
        $isMultiUnLockFailedCount = [];
        
        //利用协程的方式同时解锁多把锁，如果都成功，则成功
        foreach ($locksArr as $key => $val )
        {
            co(function () use ($wg,$key,$val,& $result,$kVal)
            {
                $event_id = $val['event_id'];
                $isUnLockOk = $this->unlock($event_id, $kVal);
                if( ! $isUnLockOk ) {
                    $isMultiUnLockFailedCount[$event_id] = 1;
                }
                $wg->done();
            });
        }
        
        $wg->wait();
        return $isMultiUnLockFailedCount ? false : true;
    }
    
    /**
     * 加锁
     * @param string $name
     * @param string $token
     * @param int $expire
     * @param int $retryTimes
     * @param float $sleep
     * 有锁情况下，睡眠$sleep,重试$retryTimes $retryTimes * $sleep 需小于 $expire 
     */
    public function lock(string $name,string $kVal, int $expire = 15, int $usleep = 30 )
    {
        $lockArr        = [];
        $isLockOk       = false;
        $key            = self::REDIS_LOCK_KEY_PREFIX . $name;
        $i              = 1;
        $retryTimes     = $expire;
        
        //循环批量处理次数
        while ( $retryTimes -- > 0 ) {
            $isLockOk = $this->getLock($key,$expire,$kVal);
            if( $isLockOk ) {
                $this->lockedNames[$key] = $kVal;
                break;
            }
            
            logger('info','lock')->info("【{$name}】重试获取锁第{$i}次");
            
            //休眠之后继续寻找锁
            usleep($usleep * 1000);
            $i++;
        }
        return $isLockOk;
    }
    
    /**
     * 获取锁
     * @param $key
     * @param $expire
     * @param $value
     * @return mixed
     */
    private function getLock($key, $expire, $value)
    {
        $script = <<<LUA
            local key = KEYS[1]
            local value = ARGV[1]
            local ttl = ARGV[2]
    
            if (redis.call('setnx', key, value) == 1) then
                return redis.call('expire', key, ttl)
            elseif (redis.call('ttl', key) == -1) then
                return redis.call('expire', key, ttl)
            end
    
            return 0
LUA;
        return $this->execLuaScript($script, [$key, $value, $expire]);
    }    
    
    /**
     * 解锁
     * @param string $name
     * @return mixed
     */
    public function unlock(string $name,$val )
    {
        $script = <<<LUA
            local key = KEYS[1]
            local value = ARGV[1]
    
            if (redis.call('exists', key) == 1 and redis.call('get', key) == value)
            then
                return redis.call('del', key)
            end
    
            return 0
LUA;
        $key = self::REDIS_LOCK_KEY_PREFIX . $name;        
        if (isset($this->lockedNames[$key])) {
            $val = $this->lockedNames[$key];
            return $this->execLuaScript($script, [$key, $val]);
        }
        
        return false;
    }    
    
    /**
     * 执行lua脚本
     * @param string $script
     * @param array  $params
     * @param int    $keyNum
     * @return mixed
     */
    private function execLuaScript($script, array $params, $keyNum = 1)
    {
        $hash = $this->redis->script('load', $script);
        return $this->redis->evalSha($hash, $params, $keyNum);
    }
}